package com.blade.embedd;

import com.blade.Blade;
import com.blade.Const;
import com.blade.exception.EmbedServerException;
import com.blade.kit.CollectionKit;
import com.blade.kit.StringKit;
import com.blade.kit.base.Config;
import com.blade.mvc.context.BladeInitListener;
import com.blade.mvc.context.DynamicContext;
import com.blade.mvc.dispatch.AsyncDispatcherServlet;
import com.blade.mvc.dispatch.DispatcherServlet;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.webapp.WebAppContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;
import java.io.File;
import java.net.URL;
import java.util.EnumSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import static com.blade.Blade.$;

/**
 * Blade Jetty Server
 *
 * @author <a href="mailto:biezhi.me@gmail.com" target="_blank">biezhi</a>
 * @since 0.0.8
 */
public class EmbedJettyServer implements EmbedServer {

    private static final Logger LOGGER = LoggerFactory.getLogger(EmbedJettyServer.class);

    private int port = Const.DEFAULT_PORT;

    private Server server;

    private String classPath;

    private WebAppContext webAppContext;

    private ServletHolder defaultHolder;

    public EmbedJettyServer() {
        System.setProperty("org.apache.jasper.compiler.disablejsr199", "true");
        if (DynamicContext.isJarContext()) {
            URL url = EmbedJettyServer.class.getResource("/");
            this.classPath = url.getPath();
            LOGGER.info("add classpath: {}", classPath);
        }
        $().enableServer(true);
    }

    @Override
    public void startup(int port) throws EmbedServerException {
        this.startup(port, "/", null);
    }

    @Override
    public void startup(int port, String contextPath) throws EmbedServerException {
        this.startup(port, contextPath, null);
    }

    @Override
    public void setWebRoot(String webRoot) {
        webAppContext.setResourceBase(webRoot);
    }

    @Override
    public void startup(int port, String contextPath, String webRoot) throws EmbedServerException {
        this.port = port;

        Config config = Blade.$().config();

        int minThreads = config.getInt("server.jetty.min-threads", 8);
        int maxThreads = config.getInt("server.jetty.max-threads", 200);

        String poolName = config.get("server.jetty.pool-name", "blade-pool");

        // Setup Threadpool
        QueuedThreadPool threadPool = new QueuedThreadPool();
        threadPool.setMinThreads(minThreads);
        threadPool.setMaxThreads(maxThreads);
        threadPool.setName(poolName);

        this.server = new org.eclipse.jetty.server.Server(threadPool);

        this.webAppContext = new WebAppContext();

        this.webAppContext.setContextPath(contextPath);
        this.webAppContext.setResourceBase("");

        int securePort = config.getInt("server.jetty.http.secure-port", 9443);
        int outputBufferSize = config.getInt("server.jetty.http.output-buffersize", 32 * 1024);
        int requestHeaderSize = config.getInt("server.jetty.http.request-headersize", 8 * 1024);
        int responseHeaderSize = config.getInt("server.jetty.http.response-headersize", 8 * 1024);

        // HTTP Configuration
        HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSecurePort(securePort);
        http_config.setOutputBufferSize(outputBufferSize);
        http_config.setRequestHeaderSize(requestHeaderSize);
        http_config.setResponseHeaderSize(responseHeaderSize);

        long idleTimeout = config.getLong("server.jetty.http.idle-timeout", 30000L);

        String host = config.get("server.host", "0.0.0.0");
        ServerConnector serverConnector = new ServerConnector(server, new HttpConnectionFactory(http_config));
        serverConnector.setHost(host);
        serverConnector.setPort(this.port);
        serverConnector.setIdleTimeout(idleTimeout);
        server.setConnectors(new Connector[]{serverConnector});

        boolean isAsync = config.getBoolean("server.async", false);

        Class<? extends Servlet> servlet = isAsync ? AsyncDispatcherServlet.class : DispatcherServlet.class;

        ServletHolder servletHolder = new ServletHolder(servlet);
        servletHolder.setAsyncSupported(isAsync);
        servletHolder.setInitOrder(1);

        webAppContext.addEventListener(new BladeInitListener());

        Set<String> statics = Blade.$().bConfig().getStatics();
        defaultHolder = new ServletHolder(DefaultServlet.class);
        defaultHolder.setInitOrder(0);
        if (StringKit.isNotBlank(classPath)) {
            LOGGER.info("add classpath : {}", classPath);
            defaultHolder.setInitParameter("resourceBase", classPath);
        }

        statics.forEach(s -> {
            if(s.indexOf(".") != -1){
                webAppContext.addServlet(defaultHolder, s);
            } else {
                s = s.endsWith("/") ? s + '*' : s + "/*";
                webAppContext.addServlet(defaultHolder, s);
            }
        });
        webAppContext.addServlet(defaultHolder, "/favicon.ico");
        webAppContext.addServlet(servletHolder, "/");

        try {

            this.loadServlets(webAppContext);
            this.loadFilters(webAppContext);

            HandlerList handlerList = new HandlerList();
            handlerList.setHandlers(new Handler[]{webAppContext, new DefaultHandler()});
            server.setHandler(handlerList);
            server.setStopAtShutdown(true);

            server.start();
            LOGGER.info("Blade Server Listen on {}:{}", host, this.port);
            server.join();
        } catch (Exception e) {
            throw new EmbedServerException(e);
        }
    }

    @Override
    public void addStatic(String... statics) {
        if(null == statics || statics.length < 1 || null == webAppContext){
            return;
        }
        for(String s : statics){
            if(s.indexOf(".") != -1){
                webAppContext.addServlet(defaultHolder, s);
            } else {
                s = s.endsWith("/") ? s + '*' : s + "/*";
                webAppContext.addServlet(defaultHolder, s);
            }
        }
    }

    @Override
    public void addServlet(Class<? extends Servlet> servlet, String pathSpec) {
        if(null != servlet){
            webAppContext.addServlet(servlet, pathSpec);
        }
    }

    @Override
    public void addFilter(Class<? extends Filter> filter, String pathSpec) {
        if(null != filter && StringKit.isNotBlank(pathSpec)){
            webAppContext.addFilter(filter, pathSpec, EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE));
        }
    }

    public void hotSwap(int scanInterval, String resBase) throws Exception {
        Scanner scanner = new Scanner();
        scanner.setScanInterval(scanInterval);
        scanner.addScanDir(new File(resBase));
        scanner.addListener((Scanner.BulkListener) fileNames -> {
            webAppContext.stop();
            webAppContext.setResourceBase(resBase);
            webAppContext.start();
            webAppContext.getHandler().start();
        });
        LOGGER.info("Hot Swap scan interval is {}s.", scanInterval);
        scanner.start();
    }

    public void loadFilters(WebAppContext webAppContext) throws Exception {
        Map<Class<? extends Filter>, String[]> filters = Blade.$().filters();
        if (CollectionKit.isNotEmpty(filters)) {
            Set<Entry<Class<? extends Filter>, String[]>> entrySet = filters.entrySet();
            for (Entry<Class<? extends Filter>, String[]> entry : entrySet) {
                Class<? extends Filter> filterClazz = entry.getKey();
                String[] pathSpecs = entry.getValue();
                for (String pathSpec : pathSpecs) {
                    webAppContext.addFilter(filterClazz, pathSpec, EnumSet.of(DispatcherType.REQUEST));
                }
            }
        }
    }

    public void loadServlets(WebAppContext webAppContext) throws Exception {
        Map<Class<? extends HttpServlet>, String[]> servlets = Blade.$().servlets();
        if (CollectionKit.isNotEmpty(servlets)) {
            Set<Entry<Class<? extends HttpServlet>, String[]>> entrySet = servlets.entrySet();
            for (Entry<Class<? extends HttpServlet>, String[]> entry : entrySet) {
                Class<? extends HttpServlet> servletClazz = entry.getKey();
                String[] pathSpecs = entry.getValue();
                for (String pathSpec : pathSpecs) {
                    webAppContext.addServlet(servletClazz, pathSpec);
                }
            }
        }
    }

    public void shutdown() throws EmbedServerException {
        try {
            server.stop();
        } catch (Exception e) {
            throw new EmbedServerException(e);
        }
    }

    @Override
    public void join() throws EmbedServerException {
        try {
            server.join();
        } catch (InterruptedException e) {
            throw new EmbedServerException(e);
        }
    }

}